﻿Imports System.Windows.Threading
Imports System.Windows.Media.Animation

Partial Public Class DiagramViewer

#Region " fields "


    ' Amount of the diagram that cannot be scrolled, this 
    ' ensures that some of the diagram is always visible.
    Private Const PanMargin As Double = 50

    ' Duration to pause before auto centering the diagram.
    Private Const AutoCenterAnimationPauseDuration As Double = 1000

    ' Duration of the auto center animation.
    Private Const AutoCenterAnimationDuration As Double = 600


    ' Used when manually scrolling.
    Private scrollStartPoint As Point
    Private scrollStartOffset As Point

    ' Stores the top-left offset of the diagram. Used to auto-scroll
    ' the new primary node to the location of the previous selected node.
    Private previousTopLeftOffset As Point

    ' Timer that is used when animating a new diagram.
    Private autoCenterTimer As DispatcherTimer

#End Region

#Region " properties "

    ''' <summary>
    ''' Get or set the zoom level of the diagram.
    ''' </summary>
    Public Property Zoom() As Double
        Get
            'Return ZoomSlider.Value
        End Get
        Set(ByVal value As Double)
            If value >= ZoomSlider.Minimum AndAlso value <= ZoomSlider.Maximum Then
                Diagram.Scale = value
                ZoomSlider.Value = value
                UpdateScrollSize()
            End If
        End Set
    End Property


#End Region

    Public Sub New()

        ' This call is required by the Windows Form Designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        ' Default zoom level.
        Me.Zoom = 1
        ' Timer used for animations.
        autoCenterTimer = New DispatcherTimer()
    End Sub

    Private Overloads Sub OnInitialized(ByVal e As EventArgs)
        ' Timer used for animations.
        '  autoCenterTimer = New DispatcherTimer()

        MyBase.OnInitialized(e)
    End Sub

#Region " Event Handlers "

    Private Sub Diagram_DiagramPopulated(ByVal sender As Object, ByVal e As System.EventArgs) Handles Diagram.DiagramPopulated
        ' The diagram was populated. Need to force the diagram to layout 
        ' since the diagram values are required to perform animations (need 
        ' to know exactly where the primary node is located).

        ' Save the current top-left offset before force the layout,
        ' this is used later when animating the diagram.
        Dim offset As Point = GetTopLeftScrollOffset()
        previousTopLeftOffset = New Point( _
            Grid.ActualWidth - ScrollViewer.HorizontalOffset - offset.X, _
            Grid.ActualHeight - ScrollViewer.VerticalOffset - offset.Y)

        ' Force the layout.
        Me.UpdateLayout()

        ' Now auto-scroll so the primary node appears at the previous
        ' selected node location.
        AutoScrollToSelected()

    End Sub

    Private Sub Diagram_DiagramUpdated(ByVal sender As Object, ByVal e As System.EventArgs) Handles Diagram.DiagramUpdated

    End Sub

    Private Sub Diagram_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Diagram.Loaded
        ' Initialize the display after the diagram has been loaded,
        ' set the scroll size and center the diagram in the display area.
        UpdateScrollSize()
        AutoScrollToSelected()
    End Sub

    Private Sub Diagram_SizeChanged(ByVal sender As Object, ByVal e As System.Windows.SizeChangedEventArgs) Handles Diagram.SizeChanged, Me.SizeChanged
        ' Update the scroll size, this is necessary to make sure the
        ' user cannot scroll the entire diagram off the display area.
        UpdateScrollSize()
    End Sub

    Private Sub ZoomSlider_MouseDoubleClick(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles ZoomSlider.MouseDoubleClick
        ' Reset the zoom level.
        ZoomSlider.Value = 1.0
    End Sub

    Private Sub ZoomSlider_ValueChanged(ByVal sender As Object, ByVal e As System.Windows.RoutedPropertyChangedEventArgs(Of Double)) Handles ZoomSlider.ValueChanged
        ' Update the diagram zoom level.
        Me.Zoom = e.NewValue
    End Sub

    Private Sub ScrollViewer_ScrollChanged(ByVal sender As Object, ByVal e As System.Windows.Controls.ScrollChangedEventArgs) Handles ScrollViewer.ScrollChanged
        ' Need to adjust the scroll position when zoom changes to keep 
        ' the diagram centered. The ScrollChanged event occurs when
        ' zooming since the diagram's extent changes.

        If Not e.ExtentWidthChange = 0 AndAlso Not e.ExtentWidthChange = e.ExtentWidth Then
            ' Keep centered horizontaly.
            Dim percent As Double = e.ExtentWidthChange / (e.ExtentWidth - e.ExtentWidthChange)
            Dim middle As Double = e.HorizontalOffset + (e.ViewportWidth / 2)
            ScrollViewer.ScrollToHorizontalOffset(e.HorizontalOffset + (middle * percent))
        End If

        If Not e.ExtentHeightChange = 0 AndAlso Not e.ExtentHeightChange = e.ExtentHeight Then
            ' Keep centered verically.
            Dim percent As Double = e.ExtentHeightChange / (e.ExtentHeight - e.ExtentHeightChange)
            Dim middle As Double = e.VerticalOffset + (e.ViewportHeight / 2)
            ScrollViewer.ScrollToVerticalOffset(e.VerticalOffset + (middle * percent))
        End If

    End Sub

#End Region

    ''' <summary>
    ''' Return the offset that positions the diagram in the top-left 
    ''' corner, takes into account the zoom level.
    ''' </summary>
    Private Function GetTopLeftScrollOffset() As Point
        ' Offset that is returned.
        Dim offset As New Point()

        ' Empty offset if the diagram is empty.
        If Diagram.ActualWidth = 0 OrElse Diagram.ActualHeight = 0 Then
            Return offset
        End If

        ' Get the size of the diagram.
        Dim diagramSize As New Size( _
            Diagram.ActualWidth * Me.Zoom, _
            Diagram.ActualHeight * Me.Zoom)

        ' Calcualte the offset that positions the diagram in the top-left corner.
        offset.X = Me.ActualWidth + diagramSize.Width - (PanMargin / 2)
        offset.Y = Me.ActualHeight + diagramSize.Height - (PanMargin / 2)

        Return offset
    End Function


    ''' <summary>
    ''' Update the scroll area so the diagram can be scrolled from edge to edge.
    ''' </summary>
    Private Sub UpdateScrollSize()
        ' Nothing to do if the diagram is empty.
        If Me.ActualWidth = 0 OrElse Me.ActualHeight = 0 Then
            Return
        End If

        Dim diagramSize As New Size( _
            Diagram.ActualWidth * Me.Zoom, _
            Diagram.ActualHeight * Me.Zoom)

        ' The grid contains the diagram, set the size of the grid so it's
        ' large enough to allow the diagram to scroll from edge to edge.
        Grid.Width = Math.Max(0, (Me.ActualWidth * 2) + diagramSize.Width - PanMargin)
        Grid.Height = Math.Max(0, (Me.ActualHeight * 2) + diagramSize.Height - PanMargin)
    End Sub


    ''' <summary>
    ''' Scroll the diagram so the primary node appears at the location of the selected node.
    ''' </summary>
    Private Sub AutoScrollToSelected()
        ' Don't scroll if the diagram is empty.
        If Diagram.ActualWidth = 0 OrElse Diagram.ActualHeight = 0 Then
            Return
        End If

        ' This is the offset that will be scrolled. First get the offset 
        ' that positions the diagram in the top-left corner.
        Dim offset As Point = GetTopLeftScrollOffset()

        ' Get the location of the node that was selected.
        Dim selectedBounds As Rect = Diagram.SelectedNodeBounds

        ' See if this is the first time the diagram is being displayed.            
        If selectedBounds.IsEmpty Then
            ' First time, center the diagram in the display area.
            offset.X += ((Me.ActualWidth - (Diagram.ActualWidth * Me.Zoom)) / 2)
            offset.Y += ((Me.ActualHeight - (Diagram.ActualHeight * Me.Zoom)) / 2)
        Else
            ' Scroll the diagram so the new primary node is at the location
            ' of the previous selected node. 

            ' Offset the distance the diagram is scrolled from the 
            ' previous top-left position.
            offset.X += previousTopLeftOffset.X
            offset.Y += previousTopLeftOffset.Y

            ' Determine the distance between the two nodes.
            Dim primaryBounds As Rect = Diagram.PrimaryNodeBounds
            Dim nodeDelta As New Point()
            nodeDelta.X = (primaryBounds.Left + (primaryBounds.Width / 2)) - _
                (selectedBounds.Left + (selectedBounds.Width / 2))
            nodeDelta.Y = (primaryBounds.Top + (primaryBounds.Height / 2)) - _
                (selectedBounds.Top + (selectedBounds.Height / 2))

            ' Offset the distance between the two nodes.
            offset.X -= (nodeDelta.X * Me.Zoom)
            offset.Y -= (nodeDelta.Y * Me.Zoom)
        End If


        ' Scroll the diagram.
        ScrollViewer.ScrollToHorizontalOffset(Grid.Width - offset.X)
        ScrollViewer.ScrollToVerticalOffset(Grid.Height - offset.Y)

        ' Set a timer so there is a pause before centering the diagram.
        autoCenterTimer.Interval = Helper.GetAnimationDuration(AutoCenterAnimationPauseDuration)
        AddHandler autoCenterTimer.Tick, AddressOf OnAutoCenterPauseTimer
        autoCenterTimer.IsEnabled = True
    End Sub

    Private Sub OnAutoCenterPauseTimer(ByVal sender As Object, ByVal e As EventArgs)
        ' Timer only fires once.
        autoCenterTimer.IsEnabled = False

        ' Scroll the diagram so it's centered in the display area.
        AutoScrollToCenter()
    End Sub

    ''' <summary>
    ''' Center the diagram in the display area.
    ''' </summary>
    Private Sub AutoScrollToCenter()
        ' Adjust the offset so the diagram appears in the center of 
        ' the display area. First get the top-left offset.
        Dim offset As Point = GetTopLeftScrollOffset()

        ' Now adjust the offset so the diagram is centered.
        offset.X += ((Me.ActualWidth - (Diagram.ActualWidth * Me.Zoom)) / 2)
        offset.Y += ((Me.ActualHeight - (Diagram.ActualHeight * Me.Zoom)) / 2)

        ' Before auto scroll, determine the start and end 
        ' points so the scrolling can be animated.
        Dim startLocation As New Point( _
            ScrollViewer.HorizontalOffset, _
            ScrollViewer.VerticalOffset)

        Dim endLocation As New Point( _
            Grid.Width - offset.X - startLocation.X, _
            Grid.Height - offset.Y - startLocation.Y)

        ' Auto scroll the diagram.
        ScrollViewer.ScrollToHorizontalOffset(Grid.Width - offset.X)
        ScrollViewer.ScrollToVerticalOffset(Grid.Height - offset.Y)

        ' Animate the scrollings.
        AnimateDiagram(endLocation)
    End Sub

    ''' <summary>
    ''' Animate the diagram by moving from the startLocation to the endLocation.
    ''' </summary>
    Private Sub AnimateDiagram(ByVal endLocation As Point)
        ' Create the animations, nonlinear by using accelration and deceleration.
        Dim horzAnim As New DoubleAnimation(endLocation.X, 0, _
            Helper.GetAnimationDuration(AutoCenterAnimationDuration))
        horzAnim.AccelerationRatio = 0.5
        horzAnim.DecelerationRatio = 0.5

        Dim vertAnim As New DoubleAnimation(endLocation.Y, 0, _
            Helper.GetAnimationDuration(AutoCenterAnimationDuration))
        vertAnim.AccelerationRatio = 0.5
        vertAnim.DecelerationRatio = 0.5

        ' Animate the transform to make it appear like the diagram is moving.
        Dim transform As New TranslateTransform()
        transform.BeginAnimation(TranslateTransform.XProperty, horzAnim)
        transform.BeginAnimation(TranslateTransform.YProperty, vertAnim)

        ' Animate the grid (that contains the diagram) instead of the 
        ' diagram, otherwise nodes are clipped in the diagram.
        Grid.RenderTransform = transform
    End Sub


    ''' <summary>
    ''' Adjust the time slider for Shift + MouseWheel, 
    ''' adjust the zoom slider for Ctrl + MouseWheel.
    ''' </summary>
    Protected Overrides Sub OnPreviewMouseWheel(ByVal e As MouseWheelEventArgs)
        ' Zoom slider.
        If (Keyboard.Modifiers And ModifierKeys.Control) > 0 Then
            e.Handled = True
            Me.Zoom += CType(IIf((e.Delta > 0), ZoomSlider.LargeChange, -ZoomSlider.LargeChange), Double)
        End If

        MyBase.OnPreviewMouseWheel(e)
    End Sub




#Region " manual scroll "

    Protected Overrides Sub OnPreviewMouseDown(ByVal e As MouseButtonEventArgs)
        If ScrollViewer.IsMouseOver AndAlso Not Diagram.IsMouseOver Then
            ' Save starting point, used later when determining how much to scroll.
            scrollStartPoint = e.GetPosition(Me)
            scrollStartOffset.X = ScrollViewer.HorizontalOffset
            scrollStartOffset.Y = ScrollViewer.VerticalOffset

            ' Update the cursor if can scroll or not.
            Me.Cursor = CType(IIf((ScrollViewer.ExtentWidth > ScrollViewer.ViewportWidth) OrElse _
                (ScrollViewer.ExtentHeight > ScrollViewer.ViewportHeight), _
                Cursors.ScrollAll, Cursors.Arrow), Input.Cursor)

            Me.CaptureMouse()
        End If

        MyBase.OnPreviewMouseDown(e)
    End Sub


    Protected Overrides Sub OnPreviewMouseMove(ByVal e As MouseEventArgs)
        If Me.IsMouseCaptured Then
            ' Get the new scroll position.
            Dim point As Point = e.GetPosition(Me)

            ' Determine the new amount to scroll.
            Dim delta As New Point( _
                CDbl(IIf((point.X > Me.scrollStartPoint.X), _
                    -(point.X - Me.scrollStartPoint.X), (Me.scrollStartPoint.X - point.X))), _
                CDbl(IIf((point.Y > Me.scrollStartPoint.Y), _
                    -(point.Y - Me.scrollStartPoint.Y), (Me.scrollStartPoint.Y - point.Y))))

            ' Scroll to the new position.
            ScrollViewer.ScrollToHorizontalOffset(Me.scrollStartOffset.X + delta.X)
            ScrollViewer.ScrollToVerticalOffset(Me.scrollStartOffset.Y + delta.Y)
        End If

        MyBase.OnPreviewMouseMove(e)
    End Sub

    Protected Overrides Sub OnPreviewMouseUp(ByVal e As MouseButtonEventArgs)
        If Me.IsMouseCaptured Then
            Me.Cursor = Cursors.Arrow
            Me.ReleaseMouseCapture()
        End If

        MyBase.OnPreviewMouseUp(e)
    End Sub


#End Region

End Class
